Sanic Flask Tornado 性能测试

这其实是公司安排的一个任务,对 Sanic 框架进行简单的研究,并对比一下目前的性能。例子中我们使用 wrk 进行简单的压力测试,对简单的 HTTP 请求进行分析。

Sanic

安装 Sanic

1
pip install sanic

编写简单的 Sanic 异步 view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text

app = Sanic('sanic_demo')


class SimpleAsyncView(HTTPMethodView):

async def get(self, request):
return text('I am async get method')

async def post(self, request):
return text('I am async post method')

async def put(self, request):
return text('I am async put method')


app.add_route(SimpleAsyncView.as_view(), '/async')

if __name__ == '__main__':
app.run(host="0.0.0.0", port=8673)

测试

介绍

这里的 web 压力测试使用 wrk, 具体使用方法可参考 wrk 。另外因为每个 web 框架都自带 Server(Sanci, Tornado 二者本身可以作为 Web Server,Flask 自带 WSGI 服务),为了保证每个 web 框架的统一性,这里都统一使用 Gunicorn 部署服务,并且 worker 数量统一为16。另外也保证环境的统一,我们使用同一个镜像来进行测试。

gunicorn 配置大致如下:

1
2
3
4
5
6
7
8
9
bind = '0.0.0.0:8673'
workers = 16
backlog = 2048
#worker_class = "gevent"
debug = False
proc_name = 'gunicorn.pid'
pidfile = 'gunicorn.pid'
logfile = 'debug.log'
loglevel='debug'
镜像
1
docker pull python-3.6
发布脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/bin/sh

# 启动类型,支持uwsgi 或 gunicorn
start_type="gunicorn"

# 容器名称
container_name="sanic_demo"

# 运行端口
run_port="8673"

# 容器内部运行端口
container_run_port="8673"

#宿主机代码地址
code_path="/Users/rex/Documents/Projects/Python/sanic_demo/sanic_part/"

# 映射到容器内部的代码地址
container_code_path="/app/docker/www/sanic_demo/"

# 镜像名称
image="sanic_demo:3.6"



kill_container(){
# 停止并且删除待发布正在运行的容器
echo "**INFO**: " 停止旧服务: $container_name
docker rm -f $container_name
echo "**INFO**: " 停止 $container_name 服务成功!:
}

pull_new_image(){
# 拉取本次最新的镜像
echo "**INFO**: " 拉取所需镜像: $image
docker pull $image
echo "**INFO**: " 拉取镜像 $image 成功!
}


publish(){
# 启动服务
echo "**INFO**: " 启动服务, 运行类型为 $start_type:

if [ "$start_type" == "uwsgi" ]
then
docker run -d -p $run_port:$container_run_port -v $code_path:$container_code_path -e SERVICE_NAME=$container_name -e SERVICE_TAGS=$container_name -w $container_code_path --name=$container_name $image nohup uwsgi --ini uwsgi.ini --wsgi-disable-file-wrapper
elif [ "$start_type" == "gunicorn" ]
then
docker run -d -p $run_port:$container_run_port -v $code_path:$container_code_path -e SERVICE_NAME=$container_name -e SERVICE_TAGS=$container_name -w $container_code_path --name=$container_name $image nohup gunicorn -c gunicorn.conf -t 60 server:app
else
echo "**INFO**: " 目前只支持 uwsgi 或 gunicorn 发布 Python 项目!
fi
}

get_publish_status(){
# 获取本次发布状态:
grep_container_name=`docker ps --format "{{.Names}}" | grep $container_name`
if [ "$grep_container_name" == "$container_name" ]
then
echo "**INFO**: " $container_name 发布成功!!!
else
echo "**INFO**: " $container_name 发布失败!!!
fi
}

get_all_container_status(){
# 查看所有容器状态
docker ps
}


main(){
kill_container
pull_new_image
publish
get_publish_status
# get_all_container_status
}

main
启动

输出日志如下:

1
2
3
4
5
6
7
8
9
10
192:docker rex$ sh publish.sh 
**INFO**: 停止旧服务: sanic_demo
sanic_demo
**INFO**: 停止 sanic_demo 服务成功!:
**INFO**: 拉取所需镜像: sanic_demo:3.6
Error response from daemon: pull access denied for sanic_demo, repository does not exist or may require 'docker login'
**INFO**: 拉取镜像 sanic_demo:3.6 成功!
**INFO**: 启动服务, 运行类型为 gunicorn:
2a2ce354f08b4d62d86f600f19f691ff8062798d00308cd0e7abb683010b77ae
**INFO**: sanic_demo 发布成功!!!

查看容器状态:

1
2
3
192:~ rex$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2a2ce354f08b sanic_demo:3.6 "nohup gunicorn -c g…" 5 seconds ago Up 3 seconds 0.0.0.0:8673->8673/tcp sanic_demo

访问 http://127.0.0.1:8673/async 却得到一个 Internal Server Error 后来查询官方文档,才知道若 sanic 使用 gunicorn 启动,那么需要加上 –worker-class sanic.worker.GunicornWorker 参数。加上后正常启动,访问页面,就能看到返回的内容了。

压力测试

安装 wrk

1
brew install wrk

并发测试

1
wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/async

这条命令的意思是用4个线程来模拟 1000 个并发连接,整个测试持续 30 秒,连接超时 30 秒,打印出请求的延迟统计信息。需要注意的是 wrk 使用异步非阻塞的 io,并不是用线程去模拟并发连接,因此不需要设置很多的线程,一般根据 CPU 的核心数量设置即可。另外 -c 参数设置的值必须大于 -t 参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
192:~ rex$ wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/async
Running 30s test @ http://127.0.0.1:8673/async
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 190.17ms 137.14ms 1.31s 78.88%
Req/Sec 311.78 97.57 700.00 69.82%
Latency Distribution
50% 163.99ms
75% 248.01ms
90% 336.62ms
99% 646.97ms
37275 requests in 30.05s, 4.98MB read
Socket errors: connect 751, read 361, write 1, timeout 0
Requests/sec: 1240.28
Transfer/sec: 169.57KB

主要关注的几个数据是

  • Socket errors socket 错误的数量
  • Requests/sec 每秒请求数量,也就是并发能力
  • Latency 延迟情况及其分布

由此可以看到使用 gunicorn 部署 Requests/sec 为 1240.28

若不使用 gunicorn 部署,而是直接启动 server.py, 测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
192:~ rex$ wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/async
Running 30s test @ http://127.0.0.1:8673/async
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 25.00ms 9.13ms 43.74ms 61.46%
Req/Sec 2.41k 289.07 4.85k 74.20%
Latency Distribution
50% 19.40ms
75% 34.88ms
90% 36.38ms
99% 39.80ms
288839 requests in 30.10s, 38.56MB read
Socket errors: connect 751, read 222, write 0, timeout 0
Requests/sec: 9595.17
Transfer/sec: 1.28MB

由此可以看到不使用 gunicorn 部署 Requests/sec 为 9595.17 刚开始我也不相信,所以这里做了两遍的测试,结果出来结果都是一样。特意去官网上查了一下,sanic 官方说明了两种部署方式,一种是直接运行,另一种使用时 gunicorn,但是并没有说明两种方式在性能上的差异性。

接下来我们可以对 Flask 进行测试

Flask

安装 Flask

1
pip install flask

编写简单的 Flask view

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask

app = Flask(__name__)


@app.route("/flask")
def hello():
return "I am flask get method"


if __name__ == "__main__":
app.run(host="127.0.0.1", port="8673")

有了 Sanic 的部署和设置,Flask 部署起来很轻松。只需将 publish.sh 中的宿主机代码地址改为 /Users/rex/Documents/Projects/Python/sanic_demo/sanic_part/ 即可。同时去除 –worker-class sanic.worker.GunicornWorker 参数。容器启动之后就能通过浏览器访问到页面了。

测试

其余准备工作和测试 Sanic 差不对,这里就直接进行压力测试了。

压力测试

并发测试

1
wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/flask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
192:~ rex$ wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/flask
Running 30s test @ http://127.0.0.1:8673/flask
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 398.12ms 833.40ms 20.42s 91.13%
Req/Sec 156.88 38.44 306.00 73.70%
Latency Distribution
50% 225.86ms
75% 242.44ms
90% 302.70ms
99% 3.90s
16589 requests in 30.10s, 2.86MB read
Socket errors: connect 751, read 446, write 2, timeout 0
Requests/sec: 551.04
Transfer/sec: 97.40KB

由此可以看到使用 gunicorn 部署 Requests/sec 为 551.04 比sanci 低了一倍多。

若不使用 gunicorn 部署,而是直接启动 server.py, 测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
192:~ rex$ wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/flask
Running 30s test @ http://127.0.0.1:8673/flask
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 63.42ms 10.12ms 93.95ms 92.39%
Req/Sec 470.13 147.27 777.00 77.49%
Latency Distribution
50% 65.15ms
75% 65.61ms
90% 66.20ms
99% 79.72ms
16574 requests in 30.05s, 2.77MB read
Socket errors: connect 763, read 833, write 9, timeout 0
Requests/sec: 551.48
Transfer/sec: 94.25KB

由此可以看到不使用 gunicorn 部署 Requests/sec 为 551.48 二者方式却别不大。

Tronado

安装 Tornado

1
pip install tornado
编写简单的 Tornado view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("I am tornado get method")


def make_app():
return tornado.web.Application([
(r"/tornado", MainHandler),
])


if __name__ == "__main__":
app = make_app()
app.listen(8673)
tornado.ioloop.IOLoop.current().start()

因为 Tornado 不使用 gunicorn 启动,Tornado 本身就是一个 HTTP Server,所以我们直接运行 server.py 进行测试就行。

测试

其余准备工作和测试 Sanic 差不对,这里就直接进行压力测试了。

压力测试

并发测试

1
wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/tornado
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
192:~ rex$ wrk -t4 -c1000 -d30s -T30s --latency http://127.0.0.1:8673/tornado
Running 30s test @ http://127.0.0.1:8673/tornado
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 78.63ms 4.31ms 88.15ms 91.13%
Req/Sec 785.93 179.12 1.19k 60.40%
Latency Distribution
50% 79.25ms
75% 79.93ms
90% 80.71ms
99% 85.43ms
93959 requests in 30.07s, 19.53MB read
Socket errors: connect 751, read 181, write 0, timeout 0
Requests/sec: 3124.51
Transfer/sec: 665.18KB

由此可以看到不使用 gunicorn 部署 Requests/sec 为 3124.51 比 Flask 高很多,比 Sanic 低很多。